#!/usr/bin/perl -w
#
# keepconfig - keep your precious config file under source control, and manage it
# GPL software by Sylvain Viart
#
# https://code.google.com/p/keepconfig/
#
# Note: perl for string manipulation, and should be present on all linux system by default
#
# current DEV:
# * --init
#

# --------------------------   DOCUMENTATION  -------------------------- {{{
=head1 NAME

keepconfig - manage your config file with SVN

=head1 SYNOPSIS

keepconfig [options] [file ...]

 Options:
   --help              this help message
   --howto             manpage like full documentation with examples
   --init DIR          create a conffile repository in the given folder
   --save              save the specified conffile into the config_map and perform the right 'svn add' command
   --download          like --save but DO NOT add the config_map entry
   --get-all [regexp]  retrieve all mfileatching file config_map
   --put               upload the local conffile on the remote server at destination matched in config_map
   --verbose|-v        increase script verbosity, 3 level are supported
   --host SERVER       use the given server but use current path for local SVN
   --test              just test if the remote file exists
   --batch             get mode only, all file are tared on the remote host and batch processed localy (faster)
   --cat               output the remote file on stdout
   --force             force remote write with --put bypassing checks


=head1 DESCRIPTION

B<keepconfig> is a script that simplify the job of getting remote config file and managing a local copy.
REMOTE and LOCAL destination can be the same host, keepconfig also provide a config_map which allow you to short or
modify the local tree to suit your needs and managing different distro.

All remote acces are performed via ssh. In order to write on the remote server, you will need to setup an ssh key pair
for root access.

All conffile are kept into an SVN local working copy (WC) all path mapping are performed by keepconfig in both way. The
WC is managed by SVN (subversion) automaticaly by keepconfig. Commit can be performed manualy or by command.

=head1 MODES

keepconfig work in 2 modes B<get> or B<put>, each use the same HOST + path mapping algorithm:
(See split_path() for the gory details)

/somewhere/machine/hostname/folder/conffile

^----------------^^-------^^--------------^
      $base       $machine     $confpath
 
currently the 'machine/' is a keyword an will be automaticaly created at init.

=item B<get mode>

Fetch the specified conffile from the remote host and place a copy into the local WC. Local not commited changes are checked
before overwriting. Ssh access can be modified with ssh.conf in each host folder. See B<CONFIG> section.

=item B<put mode>

Place the local version of the conffile on the remote host. SVN previous version are checked to ensure that remote
modified version are not overwritten. --force can be used to skip this phase.

=head1 OPTIONS

=over 8

=item B<--init> foldername

Create an empty local SVN WC on the given foldername. This foldername will be created or can be an empty folder.

Ex:  
   pwd: /home/sysadminguy/duty
   keepconfig --init keepconfig

create a new conffile WC storage in /home/sysadminguy/duty/keepconfig

=back

=cut

# -------------------------------------------------------------------------- doc END }}}

use strict;
# Getopt::Long => --help, unparsed arguments are left in @ARGV, multiple short switch Ex: -vvv
use Getopt::Long qw(:config bundling auto_help pass_through);
use Cwd;
use File::Basename;


# ---------------------------------------------------------- GLOBAL conf vars {{{
my $start_pwd;
my $EXEC_MODE='get';

# --------- unit test helper
our $fake_cwd;
our $debug_catch_error = 0;
our @debug_log = ();
our $ERROR_COUNT = 0;
our $HOME = $ENV{'HOME'};
our %OPTIONS;

# -------------------------------------------------------------- init
$OPTIONS{_force_host} = '';

# }}}

if($0 =~ /keepconfig/)
{
	my $ret_code = run_main();
	exit $ret_code;
}

# run_main
sub run_main
{
	my $param = command_line_parse();

	# keep initial PWD
	$start_pwd = cwd();

	# ================================================================= main code {{{
	SWITCH:
	{
		$EXEC_MODE eq "init" && do #{{{
		{
			init_svn_repos($param);
			last SWITCH;
		};
		#}}}
	}

	printv("FIN");
	exit 0;
	#}}}
}

# command_line_parse: perform command line parsing {{{
sub command_line_parse
{
	my $param;
	my $result = GetOptions (
		"init=s" => sub { $EXEC_MODE = 'init'; $param = $_[1]; },
		"host=s" => sub { $OPTIONS{_force_host} = $_[1]; } ,
	); 

	return $param;
}
#}}}

# split_path: split the given string argument into a location  $loc {{{
#
# argument: $fileane = the conffile to look for reading from the machine base path: machine/servername/...
# return: Hash ref
#
# /somewhere/machine/hostname/folder/conffile
# 
# ^----------------^^-------^^--------------^
#       $base       $machine     $confpath
#
# in the code this ref is always named $loc
#
# Keys:
#  'base' => $base
#  'machine' => $machine, or forced if --host was used
#  'fullpath_machine' => $base + $machine
#  'path' => $path
#
#
sub split_path
{
	my $conffile = shift;
	
	die "split_path: \$conffile was empty" if ! $conffile;

	my $fpath = get_cwd()."/$conffile";

	# loop over $fpath element and gather keys, machine/ is the start of the search
	# $n has 3 states
	#  0  : machine/ not fount
	#  1  : found
	#  >1 : collecting $path
	my $base = '';
	my $machine = '';
	my $n = 0;
	my $path = '';
	foreach my $d (split('/', $fpath))
	{
		next if ($d eq '');

		if($n == 0)
		{
			$base .= "/$d";
		}

		# detect machine/ folder, only the first occurence is considered
		# example of permitted value for $conffile: myserver-06/bind/db/machine
		if($n < 1 && $d eq 'machine')
		{
			$n = 1;
			next;
		}

		if($n >= 1)
		{
			if($n == 1)
			{
				$machine = $d;
			}
			else
			{
				if($path)
				{
					$path .= "/$d";
				}
				else
				{	
					$path = $d;
				}
			}
			$n++;
		}
	}

	# TODO: is it our job to check if we found the $machine ?
	
	# by default $machine and $server are the same, but $server can be forced by --host
	# is this case, the current SVN path $fullpath_machine is unchanged, but the remote host $server will be changed
	my $server = $machine;
	if($OPTIONS{_force_host})
	{
		$server = $OPTIONS{_force_host};
	}
	
	my $loc = {
		'base' => $base,
		'machine' => $server,
		'fullpath_machine' => "$base/$machine",
		'path' => $path,
		};

	parse_ssh_conf($loc);

	return $loc;
}
# }}}

# parse_ssh_conf: parse values from ssh.conf modify $loc {{{
# keys:
#  'ssh_param'  => $ssh_param   # default: root@$machine OR ssh_option + ssh_host
#  'ssh_host'   =>              # default: root@$machine
#  'ssh_option' =>              # default: root@$machine
sub parse_ssh_conf
{
	my $loc = shift;

	# 'option' allow to hanlde extra ssh param especialy useful to reach server behind a firewal
	# ex:
	#  command line: ssh -p 2222 -o 'HostKeyAlias my-server-name' root@123.45.67.89
	#  become: option = -p 2222 -o 'HostKeyAlias my-server-name' root@123.45.67.89

	my $ssh_conf = "$loc->{base}/$loc->{machine}/ssh.conf";
	my $ssh_host = "root\@$loc->{machine}";
	my $ssh_option = '-c blowfish';

	if(-e $ssh_conf)
	{
		open my $fh, "<$ssh_conf";
		local $_;
		while(<$fh>)
		{
			# skip comment and empty lines
			next if (/^#/ || /^\s*$/);
			
			chomp;

			if(/^option\s*=\s*(.+)/)
			{
				$ssh_option .= " $1 ";
				next;
			}

			if(/^host\s*=\s*(\S+)/)
			{
				$ssh_host = $1;
				next;
			}

			die "ssh.conf, parse error ^(option|host) = valeur\n";
		}
		close $fh;
	}

	# modify 
	$loc->{'ssh_host'} = $ssh_host;
	$loc->{'ssh_option'} = $ssh_option;
	$loc->{'ssh_param'} = $ssh_option.' '.$ssh_host;
}
# }}}

# get_cwd: unit test helper wrap cwd()#{{{
sub get_cwd
{
	if(defined($fake_cwd))
	{
		return $fake_cwd;
	}

	return cwd();
}
#}}}

# init_svn_repos: create an empty SVN WC on the given folder
sub init_svn_repos
{
	my $folder = shift;
	$ERROR_COUNT = 0;

	if(-e $folder && ! -d $folder)
	{
		error("cannot initialize an existing file");
	}

	# check empty folder
	if(-d $folder)
	{
		my @f = glob("$folder/.* $folder/*");
		my $nb_file = scalar(@f);
		if(-d "$folder/.svn" && $nb_file == 3)
		{
			# $folder is an empty SVN folder we are going to use it as our repository

			# Path: .
			# URL: https://keepconfig.googlecode.com/svn/trunk/perl
			# Repository Root: https://keepconfig.googlecode.com/svn
			# Repository UUID: a2d1dd39-8d17-afd1-5391-d6e6ef080c1a
			# Revision: 0
			# Node Kind: directory
			# Schedule: add
			my $r = `svn info`;
			create_dot_keepconfig(HOME => $HOME);
		}
		else
		{
			if($nb_file > 2)
			{
				error("cannot initialize an non empty folder: $folder");
			}
			else
			{
				# a standard (non SVN) empty folder, we just remove it and will create a new one
				rmdir $folder || error("permission denied on $folder");
			}
		}
	}

	if($ERROR_COUNT == 0)
	{
		# create empty SVN WC
		# get a repos URI
		# create or checkout the WC
		# svn co http://uri/ $folder
	}

	return ($ERROR_COUNT == 0);
}

sub error
{
	my $mesg = shift;
	if($debug_catch_error)
	{
		push(@debug_log, $mesg);
	}
	else
	{
		print STDERR "error: $mesg\n";
	}

	$ERROR_COUNT++;
}

sub create_dot_keepconfig
{
	my %param = @_;

	open my $fh, ">$HOME/.keepconfig" || die $!;
	print $fh  Dumper(\%param);
	close $fh;
}

1;
# vim: set fdm=marker:
